Goroutines and Channels

CIS 193 – Go Programming

Prakhar Bhandari, Adel Qalieh

CIS 193

Course Logistics

Concurrency with Goroutines

In the previous class, we learned about concurrency and parallelism

Now, it's time to see how to apply these concepts to Go

Goroutines

f()    // call f() and wait for it to return
go f() // create a new goroutine that calls f(), don't wait for it to return

Note: When the main function returns, all goroutines are abruptly terminated and the program exits

Goroutines and Threads

Each OS thread has a fixed-size block of memory for the stack, which can be up to 2 MB

Problematic for Go programs, which are highly concurrent and can have hundreds of thousands of goroutines

Goroutines start with a small stack space (usually 2 KB) and can grow / shrink as needed

Takeaway: Goroutines are much cheaper than threads

Demo Time

Channels

We saw in our previous lecture that there are cases when concurrent programs need to be able to communicate

A channel is a communication mechanism that lets one goroutine send values to another goroutine

Each channels has a particular type of data

Syntax

ch := make(chan int) // channels must be created before they are used with make
ch := make(chan int, 4) // 4 is the buffer length

Channels are reference types (like maps) and can be compared with == if they are of the same type - this checks if they refer to the same channel

Communicating with Channels

Channels have two main operations, send and receive

Sending transmits a value from one goroutine, through the channel, to another goroutine executing a corresponding receive operation

Send statements

ch := make(chan int)
ch <- 34   // sending 34 to the channel

Receive statements

ch := make(chan int)
<-ch       // receive with discarded result
x := <- ch // receive with saved result

Unbuffered channels

Creating a channel with make(chan T) will make an unbuffered channel

This is equivalent to initializing with make(chan T, 0)

For an unbuffered channel, a send operation will block the sending goroutine until another goroutine executes a corresponding receive operation on the same channel

Since sends and receives here wait until the other side is ready, we can use this to synchronize goroutines

Demo

Buffered Channels

Syntax

ch := make(chan T, i) // T = type, i = integer corresponding to buffer length

Send operation inserts element at back of queue, receive operation removes element from front

Can use len() and cap() to see the number of elements currently in the channel and total capacity of the channel

What gets printed?

hello := make(chan string, 3)
hello <- "1st"
hello <- "2nd"
hello <- "3rd"
fmt.Println(<-hello)
fmt.Println(<-hello)
fmt.Println(<-hello)

Buffered Channels Example

Assume we have a request() function that gets data from a url

func fastestQuery() string {
    responses := make(chan string, 3)
    go func() { responses <- request("asia.server.com") }()
    go func() { responses <- request("europe.server.com") }()
    go func() { responses <- request("americas.server.com") }()
    return <-responses // return the quickest response
}

Closing Channels

Closing a channel indicates that no more values will be sent

Syntax

close(ch)

Subsequent sends will cause a panic

Subsequent receives will yield the values that have been sent, once they run out, receives will yield the zero value of the channel type

How to check if the channel is actually closed?

v, ok := <-ch // ok is false if the channel is closed

Looping over channels

for i := range ch {...} // receives values from the channel ch until it is closed

Demo

Unidirectional Channel Types

When channels are passed into a function as an argument, they are usually either used to send or to receive

So, we can specify two different unidirectional channel types

Send-only channel

chan<- T

Receive-only channel

<-chan T

Example with Channels

func counter(out chan<- int) {
    for x := 0; x < 10; x++ { out <- x }
    close(out)
}

func squarer(out chan<- int, in <-chan int) {
    for v := range in { out <- v * v }
    close(out)
}

func printer(in <-chan int) {
    for v := range in { fmt.Println(v) }
}

func main() {
    naturals := make(chan int)
    squares := make(chan int)

    go counter(naturals)
    go squarer(squares, naturals)
    printer(squares)
}

Select

The select statement lets a goroutine wait on multiple communications

select will block until one of the cases can run, then it runs it

If there are multiple cases ready to run, one is chosen at random

The default case is run if no other case is ready

Syntax

// can be put in an infinite loop
for {
    select {
    case <-ch1:
    // ...
    case x := <-ch2:
    // ...use x...
    case ch3 <- y:
    // ...
    default:
    }
}

Bank example from last time

func Deposit(amount int) {
    mu.Lock()
    balance = balance + amount
    mu.Unlock()
}

func Balance() int {
    mu.Lock()
    b := balance
    mu.Unlock()
    return b
}

Bank example with channels

var deposits = make(chan int) // send amount to deposit
var balances = make(chan int) // receive balance

func Deposit(amount int) { deposits <- amount }
func Balance() int       { return <-balances }

func teller() {
    var balance int // balance is confined to teller goroutine
    for {
        select {
        case amount := <-deposits:
            balance += amount
        case balances <- balance:
        }
    }
}

func main() {
    go teller()
}

Race Detector

Data race detector

Homework 5

Thank you

Prakhar Bhandari, Adel Qalieh

CIS 193